#!/usr/bin/python
# -*- coding: utf-8 -*-
"""

================================================================================

    mingus - Music theory Python package, progressions module
    Copyright (C) 2008-2009, Bart Spaans

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

================================================================================

    In music and music theory you often deal with sequences
    of chords. These chord sequences are called progressions and are
    often written down using roman numerals. In this system the 'I'
    refers to the first natural triad in a key, the II to the second,
    etc. We can add prefixes and suffixes to denote more complex
    progressions, like: #V7, bIIdim7, etc.

    This module provides methods which can convert progressions
    to chords and vice versa.

================================================================================
"""

import notes
import chords
import intervals
numerals = [
    'I',
    'II',
    'III',
    'IV',
    'V',
    'VI',
    'VII',
    ]
numeral_intervals = [
    0,
    2,
    4,
    5,
    7,
    9,
    11,
    ]


def to_chords(progression, key='C'):
    """Converts a list of chord functions (eg `['I', 'V7']`) or a string (eg. 'I7') \
to a list of chords. Any number of accidentals can be used as prefix to \
augment or diminish; for example: bIV or #I. All the chord abbreviations in \
the chord module can be used as suffixes; for example: Im7, IVdim7, etc. You \
can combine prefixes and suffixes to manage complex progressions: #vii7, \
#iidim7, iii7, etc. Using 7 as suffix is ambiguous, since it is classicly \
used to denote the seventh chord when talking about progressions instead of \
_just_ the dominanth seventh chord. We have taken the classic route; I7 will \
get you a major seventh chord. If you specifically want a dominanth seventh, \
use Idom7."""

    if type(progression) == str:
        progression = [progression]
    result = []
    for chord in progression:

        # strip preceding accidentals from the string

        (roman_numeral, acc, suffix) = parse_string(chord)

        # There is no roman numeral parsing, just a simple check. Sorry to
        # disappoint. warning Should throw exception

        if roman_numeral not in numerals:
            return []

        # These suffixes don't need any post processing

        if suffix == '7' or suffix == '':
            roman_numeral += suffix

            # ahh Python. Everything is a dict.

            r = chords.__dict__[roman_numeral](key)
        else:
            r = chords.__dict__[roman_numeral](key)
            r = chords.chord_shorthand[suffix](r[0])

        while acc < 0:
            r = map(notes.diminish, r)
            acc += 1
        while acc > 0:
            r = map(notes.augment, r)
            acc -= 1
        result.append(r)
    return result


def determine(chord, key, shorthand=False):
    """Determines the harmonic function of chord in key. This function can also \
deal with lists of chords.
{{{
>>> determine([\"C\", \"E\", \"G\"], \"C\")
['tonic']
>>> determine([\"G\", \"B\", \"D\"], \"C\")
['dominant']
>>> determine([\"G\", \"B\", \"D\", \"F\"], \"C\", True)
['V7']
>>> determine([[\"C\", \"E\", \"G\"], [\"G\", \"B\", \"D\"]], \"C\", True)
[['I'], ['V']]
}}}"""

    result = []

    # Handle lists of chords

    if type(chord[0]) == list:
        for c in chord:
            result.append(determine(c, key, shorthand))
        return result

    func_dict = {
        'I': 'tonic',
        'ii': 'supertonic',
        'iii': 'mediant',
        'IV': 'subdominant',
        'V': 'dominant',
        'vi': 'submediant',
        'vii': 'subtonic',
        }
    expected_chord = [
        ['I', 'M', 'M7'],
        ['ii', 'm', 'm7'],
        ['iii', 'm', 'm7'],
        ['IV', 'M', 'M7'],
        ['V', 'M', '7'],
        ['vi', 'm', 'm7'],
        ['vii', 'dim', 'm7b5'],
        ]
    type_of_chord = chords.determine(chord, True, False, True)
    for chord in type_of_chord:
        name = chord[0]

        # Get accidentals

        a = 1
        for n in chord[1:]:
            if n == 'b':
                name += 'b'
            elif n == '#':
                name += '#'
            else:
                break
            a += 1
        chord_type = chord[a:]

        # Determine chord function

        (interval_type, interval) = intervals.determine(key, name).split(' ')
        if interval == 'unison':
            func = 'I'
        elif interval == 'second':
            func = 'ii'
        elif interval == 'third':
            func = 'iii'
        elif interval == 'fourth':
            func = 'IV'
        elif interval == 'fifth':
            func = 'V'
        elif interval == 'sixth':
            func = 'vi'
        elif interval == 'seventh':
            func = 'vii'

        # Check whether the chord is altered or not

        for x in expected_chord:
            if x[0] == func:

                # Triads

                if chord_type == x[1]:
                    if not shorthand:
                        func = func_dict[func]
                elif chord_type == x[2]:

                # Sevenths

                    if shorthand:
                        func += '7'
                    else:
                        func = func_dict[func] + ' seventh'
                else:

                # Other

                    if shorthand:
                        func += chord_type
                    else:
                        func = func_dict[func]\
                             + chords.chord_shorthand_meaning[chord_type]

        # Handle b's and #'s (for instance Dbm in key C is bII)

        if shorthand:
            if interval_type == 'minor':
                func = 'b' + func
            elif interval_type == 'augmented':
                func = '#' + func
            elif interval_type == 'diminished':
                func = 'bb' + func
        else:
            if interval_type == 'minor':
                func = 'minor ' + func
            elif interval_type == 'augmented':
                func = 'augmented ' + func
            elif interval_type == 'diminished':
                func = 'diminished ' + func

        # Add to results

        result.append(func)
    return result


def parse_string(progression):
    """Returns a tuple (roman numeral, accidentals, chord suffix).
{{{
>>> progressions.parse_string(\"I\")
('I', 0, '')
>>> progressions.parse_string(\"bIM7\")
('I', -1, 'M7')
}}}"""

    acc = 0
    roman_numeral = ''
    suffix = ''
    i = 0
    for c in progression:
        if c == '#':
            acc += 1
        elif c == 'b':
            acc -= 1
        elif c.upper() == 'I' or c.upper() == 'V':
            roman_numeral += c.upper()
        else:
            break
        i += 1
    suffix = progression[i:]
    return (roman_numeral, acc, suffix)


def tuple_to_string(prog_tuple):
    """Creates a string from tuples returned by parse_string"""

    (roman, acc, suff) = prog_tuple
    if acc > 6:
        acc = 0 - acc % 6
    elif acc < -6:
        acc = acc % 6
    while acc < 0:
        roman = 'b' + roman
        acc += 1
    while acc > 0:
        roman = '#' + roman
        acc -= 1
    return roman + suff


def substitute_harmonic(progression, substitute_index, ignore_suffix=False):
    """Does simple harmonic substitutions. Returns a list of possible substitions \
for `progression[substitute_index]`. If `ignore_suffix` is set to True the \
suffix of the chord being substituted will be ignored. Otherwise only \
progressions without a suffix, or with suffix '7' will be substituted. The \
following table is used to convert progressions:
|| I || III ||
|| I || VI ||
|| IV || II ||
|| IV || VI ||
|| V || VII ||"""

    simple_substitutions = [('I', 'III'), ('I', 'VI'), ('IV', 'II'), ('IV', 'VI'
                            ), ('V', 'VII')]
    res = []
    (roman, acc, suff) = parse_string(progression[substitute_index])
    if suff == '' or suff == '7' or ignore_suffix:
        for subs in simple_substitutions:
            r = subs[1] if roman == subs[0] else None
            if r == None:
                r = subs[0] if roman == subs[1] else None
            if r != None:
                suff = suff if suff == '7' else ''
                res.append(tuple_to_string((r, acc, suff)))
    return res


def substitute_minor_for_major(progression, substitute_index,
                               ignore_suffix=False):
    """Substitutes minor chords for its major equivalent. Recognizes 'm' and 'm7' \
suffixes and ['II', 'III', 'VI'] if there is no suffix.
{{{
>>> progressions.substitute_minor_for_major([\"VI\"], 0)
[\"I\"]
>>> progressions.substitute_minor_for_major([\"Vm\"], 0)
[\"bVIIM\"]
>>> progressions.substitute_minor_for_major([\"VIm7\"], 0)
[\"IM7\"]
}}}"""

    (roman, acc, suff) = parse_string(progression[substitute_index])
    res = []

    # Minor to major substitution

    if suff == 'm' or suff == 'm7' or suff == '' and roman in ['II', 'III', 'VI'
            ] or ignore_suffix:
        n = skip(roman, 2)
        a = interval_diff(roman, n, 3) + acc
        if suff == 'm' or ignore_suffix:
            res.append(tuple_to_string((n, a, 'M')))
        elif suff == 'm7' or ignore_suffix:
            res.append(tuple_to_string((n, a, 'M7')))
        elif suff == '' or ignore_suffix:
            res.append(tuple_to_string((n, a, '')))
    return res


def substitute_major_for_minor(progression, substitute_index,
                               ignore_suffix=False):
    """Substitutes major chords for their minor equivalent. Recognizes 'M' and 'M7' \
suffixes and ['I', 'IV', 'V'] if there is no suffix.
{{{
>>> progressions.substitute_major_for_minor([\"I\"], 0)
[\"VI\"]
>>> progressions.substitute_major_for_minor([\"VM7\"], 0)
[\"IIIm7\"]
}}}"""

    (roman, acc, suff) = parse_string(progression[substitute_index])
    res = []

    # Major to minor substitution

    if suff == 'M' or suff == 'M7' or suff == '' and roman in ['I', 'IV', 'V']\
         or ignore_suffix:
        n = skip(roman, 5)
        a = interval_diff(roman, n, 9) + acc
        if suff == 'M' or ignore_suffix:
            res.append(tuple_to_string((n, a, 'm')))
        elif suff == 'M7' or ignore_suffix:
            res.append(tuple_to_string((n, a, 'm7')))
        elif suff == '' or ignore_suffix:
            res.append(tuple_to_string((n, a, '')))
    return res


def substitute_diminished_for_diminished(progression, substitute_index,
        ignore_suffix=False):
    """Substitutes a diminished chord for another diminished chord. Recognizes the \
'dim' and 'dim7' suffix and \"VI\" if there is no suffix.
{{{
>>> progressions.substitute_diminished_for_diminished([\"VII\"], 0)
[\"IIdim\", \"bIVdim\", \"bbVIdim\"]
}}}"""

    (roman, acc, suff) = parse_string(progression[substitute_index])
    res = []

    # Diminished progressions

    if suff == 'dim7' or suff == 'dim' or suff == '' and roman in ['VII']\
         or ignore_suffix:
        if suff == '':
            suff = 'dim'

        # Add diminished chord

        last = roman
        for x in range(3):
            next = skip(last, 2)
            acc += interval_diff(last, next, 3)
            res.append(tuple_to_string((next, acc, suff)))
            last = next
    return res


def substitute_diminished_for_dominant(progression, substitute_index,
                                       ignore_suffix=False):
    (roman, acc, suff) = parse_string(progression[substitute_index])
    res = []

    # Diminished progressions

    if suff == 'dim7' or suff == 'dim' or suff == '' and roman in ['VII']\
         or ignore_suffix:
        if suff == '':
            suff = 'dim'

        # Add diminished chord

        last = roman
        for x in range(4):
            next = skip(last, 2)
            dom = skip(last, 5)
            a = interval_diff(last, dom, 8) + acc
            res.append(tuple_to_string((dom, a, 'dom7')))
            last = next
    return res


def substitute(progression, substitute_index, depth=0):
    """Gives a list of possible substitutions for `progression[substitute_index]`. \
If depth > 0 the substitutions of each result will be recursively added as \
well.
{{{
>>> progressions.substitute([\"I\", \"IV\", \"V\", \"I\"], 0)
[\"III\", \"VI\", etc.
}}}"""

    res = []
    simple_substitutions = [
        ('I', 'III'),
        ('I', 'VI'),
        ('IV', 'II'),
        ('IV', 'VI'),
        ('V', 'VII'),
        ('V', 'VIIdim7'),
        ('V', 'IIdim7'),
        ('V', 'IVdim7'),
        ('V', 'bVIIdim7'),
        ]
    p = progression[substitute_index]
    (roman, acc, suff) = parse_string(p)

    # Do the simple harmonic substitutions

    if suff == '' or suff == '7':
        for subs in simple_substitutions:
            r = None
            if roman == subs[0]:
                r = subs[1]
            elif roman == subs[1]:
                r = subs[0]
            if r != None:
                res.append(tuple_to_string((r, acc, '')))

                # Add seventh or triad depending on r

                if r[-1] != '7':
                    res.append(tuple_to_string((r, acc, '7')))
                else:
                    res.append(tuple_to_string((r[:-1], acc, '')))

    if suff == '' or suff == 'M' or suff == 'm':
        res.append(tuple_to_string((roman, acc, suff + '7')))

    if suff == 'm' or suff == 'm7':
        n = skip(roman, 2)
        a = interval_diff(roman, n, 3) + acc
        res.append(tuple_to_string((n, a, 'M')))
        res.append(tuple_to_string((n, a, 'M7')))

    # Major to minor substitution

    if suff == 'M' or suff == 'M7':
        n = skip(roman, 5)
        a = interval_diff(roman, n, 9) + acc
        res.append(tuple_to_string((n, a, 'm')))
        res.append(tuple_to_string((n, a, 'm7')))

    if suff == 'dim7' or suff == 'dim':

        # Add the corresponding dominant seventh

        res.append(tuple_to_string((skip(roman, 5), acc, 'dom7')))

        n = skip(roman, 1)
        res.append(tuple_to_string((n, acc + interval_diff(roman, n, 1), 'dom7'
                   )))

        # Add diminished chord

        last = roman
        for x in range(4):
            next = skip(last, 2)
            acc += interval_diff(last, next, 3)
            res.append(tuple_to_string((next, acc, suff)))
            last = next
    res2 = []
    if depth > 0:
        for x in res:
            new_progr = progression
            new_progr[substitute_index] = x
            res2 += substitute(new_progr, substitute_index, depth - 1)
    return res + res2


def interval_diff(progression1, progression2, interval):
    """Returns the number of half steps progression2 needs to be diminished or \
augmented until the interval between `progression1` and `progression2` is \
`interval`"""

    i = numeral_intervals[numerals.index(progression1)]
    j = numeral_intervals[numerals.index(progression2)]
    acc = 0
    if j < i:
        j += 12
    while j - i > interval:
        acc -= 1
        j -= 1
    while j - i < interval:
        acc += 1
        j += 1
    return acc


def skip(roman_numeral, skip=1):
    """Skips `skip` places to the next roman numeral.
{{{
>>> progressions.skip(\"I\")
'II'
>>> progressions.skip(\"VII\")
'I'
>>> progressions.skip(\"I\", 2)
'III'
}}}"""

    i = numerals.index(roman_numeral) + skip
    return numerals[i % 7]


